package com.example.demo.util;

import com.example.demo.annotation.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * redis分布式锁
 **/
@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
    @Resource
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.example.demo.annotation.DistributedLock)")
    public void redisLockAspect() {
    }

    @Around("redisLockAspect()")
    public void lockWithTimeOut(ProceedingJoinPoint point) throws Throwable {
        Method method = getMethod(point);
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);

        String fieldKey = parseKey(distributedLock.key(), method, point.getArgs());
        String lockV = UUID.randomUUID().toString();
        log.info("进入redis锁--- lockK {}", fieldKey);
        ValueOperations operations = redisTemplate.opsForValue();
        boolean flag = false;
        while (!flag) {
            flag = operations.setIfAbsent(fieldKey, lockV);
            if (flag) {
                log.info("获取锁成功--- " + fieldKey);
                redisTemplate.expire(fieldKey, 60, TimeUnit.SECONDS);
            }
        }
        try {
            point.proceed();
        } catch (Exception e) {
            log.error("", e);
        } finally {
            if (operations.get(fieldKey).equals(lockV)) {
                redisTemplate.delete(fieldKey);
                log.info("释放锁--- " + fieldKey);
            }
        }
    }

    /**
     * 获取缓存的key
     * key 定义在注解上，支持SPEL表达式
     *
     * @return
     */
    private String parseKey(String key, Method method, Object[] args) {
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);

        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(key).getValue(context, String.class);
    }

    /**
     * 获取被拦截方法对象
     * <p>
     * MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象
     * 而缓存的注解在实现类的方法上
     * 所以应该使用反射获取当前对象的方法对象
     */
    public Method getMethod(ProceedingJoinPoint pjp) {
        //获取参数的类型
        Object[] args = pjp.getArgs();
        Class[] argTypes = new Class[pjp.getArgs().length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }
        Method method = null;
        try {
            method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return method;
    }
}
